package org.geostats;

import static org.geoscrape.util.ParamParser.getParameter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.TimeZone;

import org.geoscrape.Attribute;
import org.geoscrape.Cache;
import org.geoscrape.CacheLog;
import org.geoscrape.Cacher;
import org.geoscrape.ListSearcher;
import org.geoscrape.LogType;
import org.geoscrape.Login;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;

/**
 * Grabs all finds or hides by a user and exports it as GPX.
 * 
 */
public class GpxExporter
{

	private static String idDatabaseString = "id.db";

	private HashMap<String, Long> userIds;

	/**
	 * @param args
	 * @throws IOException
	 * @throws ParseException
	 * @throws SAXException
	 * @throws ClassNotFoundException
	 */
	public static void main(String[] args) throws IOException, ParseException,
			SAXException, ClassNotFoundException
	{
		String name = getParameter("n", args);
		String password = getParameter("p", args);
		String outputFile = getParameter("o", args);
		if (verifyInputs(name, password, outputFile))
		{
			Login login = new Login(name, password);
			login.authenticate();
			String target = getParameter("t", args);
			if (target == null)
			{
				target = name;
			}
			GpxExporter exporter = new GpxExporter();
			int minSeconds = 0;
			int maxSeconds = 0;
			String delays = getParameter("r", args);
			if (delays != null)
			{
				String[] dparts = delays.split("-");
				if (dparts.length != 2)
				{
					System.out
							.println("Random delays must be given in the format <lowest>-<highest>");
					System.exit(1);
				}
				minSeconds = Integer.parseInt(dparts[0]);
				maxSeconds = Integer.parseInt(dparts[1]);
			}
			boolean hides = getParameter("h", args) != null;
			String count =getParameter("c",args);
			int maxCaches = 0;
			if(count != null)
			{
				maxCaches = Integer.parseInt(count);
			}
			exporter.doExport(login, target, outputFile, minSeconds, maxSeconds, hides,maxCaches);
			System.exit(0);
		}
		else
		{
			System.exit(1);
		}
	}

	@SuppressWarnings("unchecked")
	private void loadDb() throws ClassNotFoundException
	{
		try
		{
			File f = new File(idDatabaseString);
			if (f.exists())
			{
				ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
				userIds = (HashMap<String, Long>) ois.readObject();
				ois.close();
			}
		}
		catch (IOException e)
		{
			// fail silently
		}
		finally
		{
			// don't leave this method without creating a user id database
			if (userIds == null)
			{
				userIds = new HashMap<String, Long>();
			}
		}
	}

	private void saveDb()
	{
		try
		{
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
					idDatabaseString, false));
			oos.writeObject(userIds);
			oos.close();
		}
		catch (IOException e)
		{
			// fail silently
		}
	}

	/**
	 * Sanity checks on the required input values.
	 * 
	 * @param name
	 * @param password
	 * @param outputFile
	 */
	private static boolean verifyInputs(String name, String password, String outputFile)
	{
		boolean allCorrect = true;
		if (name == null || name == "")
		{
			System.out.println("You must supply a user name.");
			allCorrect = false;
		}
		if (password == null)
		{
			System.out.println("You must supply a password.");
			allCorrect = false;
		}
		if (outputFile == null)
		{
			System.out.println("You must supply an output file name.");
			allCorrect = false;
		}
		if (!allCorrect)
		{
			System.out.println();
			System.out.println("Usage:");
			System.out
					.println("GpxExporter -n <username> -p <password> -o <output filename> [-d <date format>] [-h] [-t <target name>] [-r <time range>] [-c <count>]");
			System.out.println(" Parameters:");
			System.out.println(" -n The GC.com username to extract finds for.");
			System.out.println(" -p The password for the given username.");
			System.out
					.println(" -o The output file name. If the file exists it will be overwritten.");
			System.out
					.println(" -t The geocacher alias to get finds for. If none is given you'll get your own finds.");
			System.out
					.println(" -r The time-delay range to use, on the format <lowest>-<highest>. Measured in seconds.");
			System.out.println("    Example: -r 4-28");
			System.out
					.println("    which gives random delays for each request between 4 and 28 seconds.");
			System.out
					.println("    If this parameter is not given, no delay will be used.");
			System.out
			.println("-h If this parameter is present, list a user's hides instead.");
			System.out
			.println("-c If this parameter is present, only list the latest logs.");
		}
		return allCorrect;
	}

	public void doExport(Login login, String target, String outputFile, int minSeconds,
			int maxSeconds, boolean hides, int maxCaches) throws IOException, ParseException,
			SAXException, ClassNotFoundException
	{
		loadDb();
		Cacher targetCacher = new Cacher(target);
		ListSearcher s = new ListSearcher(login);
		if (minSeconds <= maxSeconds && maxSeconds > 0)
		{
			s.setMinSeconds(minSeconds);
			s.setMaxSeconds(maxSeconds);
			s.setUseRandomDelay(true);
		}

		if (!login.getUserName().equals(targetCacher.getName()))
		{
			targetCacher.populate(login);
			String profilePage = targetCacher.getProfilePage();
			if (profilePage == null)
			{
				System.out.println("Could not find user \"" + targetCacher.getName()
						+ "\".");
				return;
			}
		}

		List<Cache> found = null;
		if (!hides)
		{
			found = s.findCachesFoundBy(targetCacher,maxCaches);
		}
		else
		{
			found = s.findCachesOwnedBy(targetCacher,0);
		}
		System.out.print(targetCacher.getName());
		if (hides)
		{
			System.out.print(" hid ");
		}
		else
		{
			System.out.print(" found ");
		}
		System.out.println(found.size() + " caches.");
		int count = 0;
		for (Cache c : found)
		{
			count++;
			System.out.println("Populating " + c.getCacheCode() +" "+ (Math.round(100000*(count/(double)found.size()))/1000.0)+"%");
			s.doRandomDelay();
			c.populate(login, hides);
			if (c.isUnavailableToUs())
			{
				System.out
						.println(c.getCacheCode()
								+ " is a premium-only cache. Log in as a premium member to grab information about it.");
			}
			else
			{
				// check if "our" log was found
				while (!hasLog(c, targetCacher) && !hides)
				{
					// log not found, load full logs
					s.doRandomDelay();
					boolean more = c.retrieveMoreLogs(login);
					if (!more)
					{
						// no more logs available
						break;
					}
				}
			}
		}

		// create a GPX file

		FileOutputStream fos = new FileOutputStream(outputFile);
		OutputFormat of = new OutputFormat("XML", "UTF-8", true);
		of.setIndent(1);
		of.setIndenting(true);
		XMLSerializer serializer = new XMLSerializer(fos, of);
		// SAX2.0 ContentHandler.
		ContentHandler hd = serializer.asContentHandler();
		hd.startDocument();
		AttributesImpl atts = new AttributesImpl();
		// gpx tag.
		atts.addAttribute("", "", "xmlns:xsi", "",
				"http://www.w3.org/2001/XMLSchema-instance");
		atts.addAttribute("", "", "xmlns:xsd", "", "http://www.w3.org/2001/XMLSchema");
		atts.addAttribute("", "", "version", "", "1.0");
		atts.addAttribute("", "", "creator", "", "Groundspeak Pocket Query");
		atts.addAttribute(
				"",
				"",
				"xsi:schemaLocation",
				"",
				"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd http://www.groundspeak.com/cache/1/0/1 http://www.groundspeak.com/cache/1/0/1/cache.xsd");
		atts.addAttribute("", "", "xmlns", "", "http://www.topografix.com/GPX/1/0");
		hd.startElement("", "", "gpx", atts);

		// preamble
		putElement(hd, "name", "My Finds Pocket Query");
		putElement(hd, "desc", "Geocache file generated by Groundspeak");
		putElement(hd, "author", "Groundspeak");
		putElement(hd, "email", "contact@groundspeak.com");
		Date d = new Date();
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S'Z'");
		dateFormat.setTimeZone(TimeZone.getTimeZone("Z"));
		putElement(hd, "time", dateFormat.format(d));
		putElement(hd, "keywords", "cache, geocache, groundspeak");

		dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
		dateFormat.setTimeZone(TimeZone.getTimeZone("Z"));

		// find the ranges of latitude and longitude
		double minLat = Double.POSITIVE_INFINITY;
		double minLon = Double.POSITIVE_INFINITY;
		double maxLat = Double.NEGATIVE_INFINITY;
		double maxLon = Double.NEGATIVE_INFINITY;
		for (Cache c : found)
		{
			if (!c.isUnavailableToUs())
			{
				double lat = c.getLocation().getLatitude().getDegreeWithFraction();
				double lon = c.getLocation().getLongitude().getDegreeWithFraction();
				minLat = Math.min(minLat, lat);
				minLon = Math.min(minLon, lon);
				maxLat = Math.max(maxLat, lat);
				maxLon = Math.max(maxLon, lon);
			}
		}
		atts.clear();
		atts.addAttribute("", "", "minlat", "", Double.toString(minLat));
		atts.addAttribute("", "", "minlon", "", Double.toString(minLon));
		atts.addAttribute("", "", "maxlat", "", Double.toString(maxLat));
		atts.addAttribute("", "", "maxlon", "", Double.toString(maxLon));
		hd.startElement("", "", "bounds", atts);
		hd.endElement("", "", "bounds");

		// add all the logs
		for (Cache c : found)
		{
			if (c.isUnavailableToUs())
			{
				// skip premium caches if we are not a premium user
				continue;
			}
			// add coordinates
			atts.clear();
			atts.addAttribute("", "", "lat", "", c.getLocation().getLatitude()
					.toDecimalString());
			atts.addAttribute("", "", "lon", "", c.getLocation().getLongitude()
					.toDecimalString());

			// add the cache details
			String cacheType = c.getCacheType().getDescription();
			hd.startElement("", "", "wpt", atts);
			putElement(hd, "time", dateFormat.format(new Date(c.getPlacedAt())));
			putElement(hd, "name", c.getCacheCode());

			String desc = c.getName() + " by " + c.getHider().getName();
			desc += ", " + cacheType + " (";
			desc += Double.toString(c.getDifficultyRating()) + "/"
					+ Double.toString(c.getTerrainRating()) + ")";
			putElement(hd, "desc", desc);
			putElement(
					hd,
					"url",
					"http://www.geocaching.com/seek/cache_details.aspx?guid="
							+ c.getGuid());
			putElement(hd, "urlname", c.getName());
			putElement(hd, "sym", "Geocache Found");
			putElement(hd, "type", "Geocache|" + cacheType);

			atts.clear();
			atts.addAttribute("", "", "id", "", Long.toString(c.getId()));
			atts.addAttribute("", "", "available", "", "True");
			if (c.isArchived())
			{
				atts.addAttribute("", "", "archived", "", "True");
			}
			else
			{
				atts.addAttribute("", "", "archived", "", "False");
			}
			atts.addAttribute("", "", "xmlns:groundspeak", "",
					"http://www.groundspeak.com/cache/1/0/1");
			hd.startElement("", "", "groundspeak:cache", atts);
			putElement(hd, "groundspeak:name", c.getName());
			putElement(hd, "groundspeak:placed_by", c.getHider().getName());
			atts.clear();
			atts.addAttribute("", "", "id", "", Long.toString(getId(c.getHider(), login)));
			putElement(hd, "groundspeak:owner", c.getHider().getName(), atts);
			putElement(hd, "groundspeak:type", cacheType);
			putElement(hd, "groundspeak:container", c.getCacheSize().toString());
			hd.startElement("", "", "groundspeak:attributes", null);

			for (Attribute a : c.getAttributes())
			{
				atts.clear();
				atts.addAttribute("", "", "id", "", Integer.toString(a.getId()));
				atts.addAttribute("", "", "inc", "", Integer.toString(a.getInc()));
				putElement(hd, "groundspeak:attribute", a.toString(), atts);
			}
			hd.endElement("", "", "groundspeak:attributes");

			putElement(hd, "groundspeak:difficulty",
					Double.toString(c.getDifficultyRating()));
			putElement(hd, "groundspeak:terrain", Double.toString(c.getTerrainRating()));

			String country = "";
			String state = "";
			String locDesc = c.getLocationDescription();
			if (locDesc.contains(","))
			{
				String[] parts = locDesc.split(",");
				state = parts[0].trim();
				country = parts[1].trim();
			}
			else
			{
				country = locDesc;
			}
			putElement(hd, "groundspeak:country", country);
			putElement(hd, "groundspeak:state", state);

			atts.clear();
			atts.addAttribute("", "", "html", "", "True");
			putElement(hd, "groundspeak:short_description", c.getShortDescription(), atts);
			putElement(hd, "groundspeak:long_description", c.getLongDescription(), atts);
			putElement(hd, "groundspeak:encoded_hints", c.getHint());

			// add the log details
			hd.startElement("", "", "groundspeak:logs", null);
			for (CacheLog log : c.getLogs())
			{
				if (hides || log.getLoggedBy().getName().equals(targetCacher.getName()))
				{
					atts.clear();
					atts.addAttribute("", "", "id", "", Long.toString(log.getId()));
					hd.startElement("", "", "groundspeak:log", atts);
					putElement(hd, "groundspeak:date",
							dateFormat.format(new Date(log.getLogTime())));
					putElement(hd, "groundspeak:type", log.getLogType().getText());
					atts.clear();
					atts.addAttribute("", "", "id", "",
							Long.toString(getId(log.getLoggedBy(), login)));
					putElement(hd, "groundspeak:finder", log.getLoggedBy().getName(),
							atts);
					atts.clear();
					atts.addAttribute("", "", "encoded", "", "False");
					putElement(hd, "groundspeak:text", log.getText(), atts);

					hd.endElement("", "", "groundspeak:log");
				}
			}

			hd.endElement("", "", "groundspeak:logs");
			hd.endElement("", "", "groundspeak:cache");
			hd.endElement("", "", "wpt");
		}
		hd.endElement("", "", "gpx");
		hd.endDocument();
		fos.close();
		saveDb();
	}

	private boolean hasLog(Cache c, Cacher targetCacher)
	{
		for (CacheLog log : c.getLogs())
		{
			if (log.getLoggedBy().getName().equals(targetCacher.getName()))
			{
				if (log.getLogType().equals(LogType.FOUND_IT)
						|| log.getLogType().equals(LogType.WEBCAM_PHOTO_TAKEN))
				{
					return true;
				}
			}
		}
		return false;
	}

	private Long getId(Cacher cacher, Login login) throws IOException
	{
		// check cache for id
		Long res = userIds.get(cacher.getProfilePage());
		if (res == null)
		{
			// find the id
			System.out.println("Getting id for user " + cacher.getName());
			cacher.populate(login);
			// store it
			res = cacher.getId();
			userIds.put(cacher.getProfilePage(), res);
		}
		return res;
	}

	private void putElement(ContentHandler hd, String name, String content)
			throws SAXException
	{
		putElement(hd, name, content, null);
	}

	private void putElement(ContentHandler hd, String name, String content,
			AttributesImpl atts) throws SAXException
	{
		hd.startElement("", "", name, atts);
		if (content != null)
		{
			hd.characters(content.toCharArray(), 0, content.length());
		}
		hd.endElement("", "", name);
	}
}
